/**
 * Copyright (C) 2009-2012, KingHool.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses.
 */
package com.kinghool.smodel.io.bean;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import com.kinghool.ebos.commons.Platform;
import com.kinghool.ebos.commons.exception.ErrorCodeRuntimeException;
import com.kinghool.ebos.commons.utils.Assert;
import com.kinghool.ebos.commons.utils.MethodUtils;
import com.kinghool.ebos.commons.utils.Utils;
import com.kinghool.smodel.ErrorCodes;
import com.kinghool.smodel.IModel;
import com.kinghool.smodel.ITreeModel;
import com.kinghool.smodel.IXmlModel;
import com.kinghool.smodel.io.IModelChannel;

/**
 * 
 * @Author Allen Deng(dengtailin@gmail.com) 
 */
public class BeanChannel implements IModelChannel {
    private String rootId;
	private Object bean;
	private IBeanFactory beanFactory;
	
	private Map<String, String> propAttrPairs = Utils.newMap();
	private Map<String, String> propChildPairs = Utils.newMap();
	private Map<String, String> propTextPairs = Utils.newMap();
	private Map<String, IModelChannel> propChannels = Utils.newMap();

	private Map<String, String> arrayPropAddMethodNames = Utils.newMap();
	private Map<String, String> arrayPropGetMethodNames = Utils.newMap();
	private Map<String, String> arrayPropSetMethodNames = Utils.newMap();
	private Map<String, String> arrayPropClearMethodNames = Utils.newMap();
	private Map<String, String> arrayPropsNodeNames = Utils.newMap();
 
	private Map<String, IModelChannel> arrayPropChannels = Utils.newMap();
	private Map<String, String> arrayTxtChildPairs = Utils.newMap(); 
	private Map<String, Class<?>> arrayTxtChildClassPairs = Utils.newMap(); 
	 
	public BeanChannel(Class<?> beanClass) {
		this(new DefaultBeanFactory(beanClass));
	} 
	
	public BeanChannel(Class<?> beanClass, String rootId) {
		this(new DefaultBeanFactory(beanClass), rootId);
	}
	
	public BeanChannel(IBeanFactory beanFactory) {
		this(beanFactory, beanFactory.getClassName());
	}
	
	public BeanChannel(IBeanFactory beanFactory, String rootId) {
		this.beanFactory = beanFactory;
		this.rootId = rootId;
	}
	
	public Object getMappedObject() {
		if (bean == null) {
			bean = beanFactory.newBean();
		}
		return bean;
	}

	public void setMappedObject(Object mapObj) {
		this.bean = mapObj;
	}
 
	public void setRootId(String rootId) {
		this.rootId = rootId;
	}

	public String getRootId() {
		return rootId;
	} 
	
	/**
	 * define the object field named as <code>property</code> mapping to attribute named as <code>property</code> of IModel
	 */ 
	public void propertyAsAttribute(String property) {
		propertyAsChildAttribute(property, null, property);
	}
	
	/**
	 * define the object field named as <code>property</code> mapping to attribute named as <code>attribute</code> of IModel
	 */ 
	public void propertyAsAttribute(String property, String attribute) {
		propertyAsChildAttribute(property, null, attribute);
	}
	
	/**
	 * define the object field named as <code>property</code> mapping to child attribute named as <code>attribute</code> of ITreeModel
	 * the child is queried by the query string:<code>childQueryStr</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
	public void propertyAsChildAttribute(String property, String childQueryStr, String attribute) {
		if (Assert.isNotNullNotEmpty(property) && Assert.isNotNullNotEmpty(attribute)) {
			if (Assert.isNotNullNotEmpty(childQueryStr)) {
				propChildPairs.put(property, childQueryStr);
			}
			propAttrPairs.put(property, attribute);
		}
	}
	
	/**
	 * define the object field named as <code>property</code> mapping to text value of IXmlModel
	 * if the model is not IXmlModel, the mapping is ignored.
	 */ 
	public void propertyAsText(String property) {
		propertyAsChildText(property, null);
	}
    
	/**
	 * define the object field named as <code>property</code> mapping to child text value of IXmlModel
	 * the child is queried by the query string:<code>property</code>
	 * if the model is not IXmlModel, the mapping is ignored.
	 */ 
    public void propertyAsChildText(String property) {
    	propertyAsChildText(property, property);
	}
	
	/**
	 * define the object field named as <code>property</code> mapping to child text value of IXmlModel
	 * the child is queried by the query string:<code>childQueryStr</code>
	 * if the model is not IXmlModel, the mapping is ignored.
	 */ 
    public void propertyAsChildText(String property, String childQueryStr) {
    	if (Assert.isNotNullNotEmpty(property)) {
			propTextPairs.put(property, childQueryStr);
		}
	}
    
	/**
	 * define the object field named as <code>property</code> mapping to child of ITreeModel
	 * The mapping logic is defined by <code>channel</code>
	 * the child is queried by the query string:<code>property</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
    public void propertyAs(String property, IModelChannel channel) {
    	propertyAs(property, property, channel);
    }
    

	/**
	 * define the object field named as <code>channel.getRootId()</code> mapping to child of ITreeModel
	 * The mapping logic is defined by <code>channel</code>
	 * the child is queried by the query string:<code>channel.getRootId()</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
    public void propertyAs(IModelChannel channel) {
    	String property = channel.getRootId();
    	propertyAs(property, property, channel);
    }
    
	/**
	 * define the object field named as <code>property</code> mapping to child of ITreeModel
	 * The mapping logic is defined by <code>channel</code>
	 * the child is queried by the query string:<code>childQueryStr</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
    public void propertyAs(String property, String childQueryStr, IModelChannel channel) {
    	if (Assert.isNotNullNotEmpty(property) && channel != null) {
    		if (Assert.isNullOrEmpty(channel.getRootId())) {
    			channel.setRootId(property);
    		}
			propChildPairs.put(property, Assert.isNotNullNotEmpty(childQueryStr) ? childQueryStr : property);
    		propChannels.put(property, channel);
		}
    }
    
	/**
	 * define the object array field named as <code>channel.getRootId()</code> mapping to child of ITreeModel
	 * array property type includes: Collection, Object[]
	 * The mapping logic is defined by <code>channel</code>
	 * the child is queried by the query string:<code>getArrayPropertyNodeName(property) + "/" + channel.getRootId()</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
    public void arrayPropertyAs(IModelChannel channel) {
    	String property = channel.getRootId() + "s";
    	arrayPropertyAs(property, channel);
    } 
    
    
	/**
	 * define the object array field named as <code>property</code> mapping to child of ITreeModel
	 * array property type includes: Collection, Object[]
	 * The mapping logic is defined by <code>channel</code>
	 * the child is queried by the query string:<code>getArrayPropertyNodeName(property) + "/" + property</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
    public void arrayPropertyAs(String property, IModelChannel channel) {
    	arrayPropertyAs(property, null, channel);
    } 
    
	/**
	 * define the object array field named as <code>property</code> mapping to child of ITreeModel
	 * array property type includes: Collection, Object[]
	 * The mapping logic is defined by <code>channel</code>
	 * the child is queried by the query string:<code>getArrayPropertyNodeName(property) + "/" + childQueryStr</code>
	 * if the model is not ITreeModel, the mapping is ignored.
	 */ 
    public void arrayPropertyAs(String property, String childQueryStr, IModelChannel channel) {
    	if (Assert.isNotNullNotEmpty(property)
    		&& channel != null) {
    		arrayPropChannels.put(property, channel);
			propChildPairs.put(property, Assert.isNotNullNotEmpty(childQueryStr) ? childQueryStr : getDefaultArrayChildFullName(property));
    		arrayPropMethods(property, null, null, null, null);
    	}
    }
    
	/**
	 * define the object array field named as <code>property</code> mapping to child text of IXmlModel
	 * array property type includes: Collection, Object[]
	 * the child is queried by the query string:<code>getArrayPropertyNodeName(property) + "/" + childQueryStr</code>
	 * if the model is not IXmlModel, the mapping is ignored.
	 */ 
    public void arrayPropertyAsChildText(String property, String childQueryStr, Class<?> clazz) {
    	if (Assert.isNotNullNotEmpty(property) 
    		|| Assert.isNotNullNotEmpty(childQueryStr)
    		|| clazz != null) {
    		arrayTxtChildPairs.put(property, childQueryStr);
    		arrayTxtChildClassPairs.put(property, clazz);
    		arrayPropMethods(property, null, null, null, null);
    	}
    }
    
    /**
     * set the child node name of the ITreeModel for an array property 
     */
    public void arrayPropertyNodeName(String property, String nodeName) {
    	if (Assert.isNotNullNotEmpty(property)) {
    		arrayPropsNodeNames.put(property, nodeName);
    	}
    }
 
    /**
     * set the addMethodName, getMethodName, clearMethodName for an array property of a Object
     * i.e, for the property named as id
     * default addMethodName is: addId
     * default getMethodName is: getIds
     * default setMethodName is: setIds
     * default clearMethodName is clear, if clear method does not exists, use getIds().clear() to reset property
     */
    public void arrayPropMethods(String property, String addMethodName, String setMethodName, String getMethodName, String clearMethodName) {
		if (Assert.isNullOrEmpty(getMethodName)) {
			getMethodName = getDefaultGetArrayMethodName(property);
		}	
		if (Assert.isNullOrEmpty(setMethodName)) {
			setMethodName = getDefaultSetArrayMethodName(property);
		}
	
		arrayPropGetMethodNames.put(property, getMethodName);
		arrayPropSetMethodNames.put(property, setMethodName);
		arrayPropAddMethodNames.put(property, addMethodName);
		arrayPropClearMethodNames.put(property, clearMethodName);
    }
    
	public void flushModel(IModel model) {
		this.setMappedObject(null);
		syncModelWithObject(model, getMappedObject(), true);
		syncModelWithObjectArray(model, getMappedObject(), true);
	}
	 
	public void loadModel(IModel model) {
		syncModelWithObject(model, getMappedObject(), false);
		syncModelWithObjectArray(model, getMappedObject(), false);
	}
    
	protected void syncModelWithObject(IModel model, Object obj, boolean model2Obj) {
     	for (String prop : propAttrPairs.keySet()) {
        	if (model instanceof ITreeModel || model instanceof IXmlModel) {
        		BeanChannelHelper.syncObjectPropertyByChildAttribute(obj, (ITreeModel) model, propChildPairs.get(prop), prop, propAttrPairs.get(prop), model2Obj);
        	} else {
        		if (Assert.isNullOrEmpty(propChildPairs.get(prop))) {
        			BeanChannelHelper.syncObjectPropertyByAttribute(obj, model, prop, propAttrPairs.get(prop), model2Obj);
        		}
        	}
		}
        if (model instanceof IXmlModel) {
	        for (String prop : propTextPairs.keySet()) {
	        	BeanChannelHelper.syncObjectPropertyByChildText(obj, (IXmlModel) model, propTextPairs.get(prop), prop, model2Obj);
	        }
        }
        
        if (model instanceof ITreeModel) {
	        for (String prop : propChannels.keySet()) {
	        	IModelChannel channel = propChannels.get(prop);
	        	ITreeModel childModel = ((ITreeModel)model).getFirstTreeChild(propChildPairs.get(prop));
	        	
	        	if (model2Obj) {
	        		if (childModel != null) {
	        			channel.flushModel(childModel);
		        		BeanChannelHelper.syncBean2Object(obj, prop, channel.getMappedObject());
	        		}
	        	} else {
	        		Object propBean = BeanChannelHelper.getObjectProperty(obj, prop);

	        		if (propBean != null) {
		        		if (childModel == null) {
			        		childModel = ((ITreeModel)model).addChild(propChildPairs.get(prop));
			        	}
		        		
		        		channel.setMappedObject(propBean);
	        		    channel.loadModel(childModel);
	        		}
	        	}
	        }
        }
	}
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	protected void model2ObjectArray(ITreeModel model, Object obj, String prop, String childName, Model2ObjectAction action) {
		clearObjectPropertyArray(obj, prop);
		
		ITreeModel propsNode = (ITreeModel) model;
		if (Assert.isNotNullNotEmpty(getArrayPropertyNodeName(prop))) {
    	    propsNode = ((IXmlModel)model).getFirstTreeChild(getArrayPropertyNodeName(prop));
		}
		if (propsNode != null) {
			childName = Assert.isNotNullNotEmpty(childName) ? childName : getDefaultArrayChildFullName(prop);
			List<Object> values = Utils.newList();
			for (ITreeModel childNode : propsNode.getTreeChild(childName)) {
				Object value = action.run(childNode, obj);
				if (value == null) continue;

				if (value != null) {
					values.add(value);
				}
			}
			
			boolean added = false;
			//add method exist, use add method first
			String addMethodName = arrayPropAddMethodNames.get(prop);
			if (Assert.isNotNullNotEmpty(addMethodName)) {
				Method addMethod = MethodUtils.getMethod(obj.getClass(), addMethodName, 1);
				if (addMethod != null) {
					try {
						for (Object value : values) {
							if (value != null) {
								MethodUtils.invokeMethod(obj, addMethod, value);
							}
						}
						added = true;
					} catch (Exception e) {
						Platform.getLogManager().error("Fail to execute method " + addMethod.getName() + " of object" + obj.getClass(), e, this);
					} 
				}
			}
				
			//add method not exist, use set method
			Method setArrayMethod = MethodUtils.getMethod(obj.getClass(), arrayPropSetMethodNames.get(prop), 1);
			if (setArrayMethod != null) {
				Class<?> arrayType = setArrayMethod.getParameterTypes()[0];
				try {
					if (Collection.class.isAssignableFrom(arrayType)) {
						Collection _values = Utils.newCollection((Class<? extends Collection<?>>) arrayType);
						_values.addAll(values);
						MethodUtils.invokeMethod(obj, setArrayMethod, _values);
					} else if (Object[].class.isAssignableFrom(arrayType)) {
						if (values.size() > 0) {
							Object array = Array.newInstance(values.get(0).getClass(), values.size());    
					        for (int i = 0; i < values.size(); i++) {
						       	Array.set(array, i, values.get(i));  
						    }
							MethodUtils.invokeMethod(obj, setArrayMethod, array);
						}
					}
					added = true;
				} catch (Exception e) {
					Platform.getLogManager().error("Fail to execute method " + setArrayMethod.getName() + " of object" + obj.getClass(), e, this);
				} 
			} 

			//if all method not exist, set value by fields directly
			if (!added) {					
				Field field = getFieldSilently(obj.getClass(), prop);
				if (field != null) {
					BeanChannelHelper.setObjectPropertyArray(obj, field, values);
				}
			}
		}
	}
	
	protected void syncModelWithObjectArray(IModel model, Object obj, boolean model2Obj) {
		if (model instanceof ITreeModel) {
            for (final String prop : arrayPropChannels.keySet()) {
            	final IModelChannel channel = arrayPropChannels.get(prop);

	        	if (model2Obj) {
    				model2ObjectArray((ITreeModel) model, obj, prop, propChildPairs.get(prop), new Model2ObjectAction(){
						public Object run(ITreeModel childNode, Object obj) {
							channel.flushModel(childNode);
    						return channel.getMappedObject();
						}
					});
	        	} else {
                    objArray2Model((ITreeModel)model, obj, prop, new Object2ModelAction(){
						public void run(ITreeModel propsNode, Object propArrayItem) {
	        				ITreeModel childModel = propsNode.addChild(propChildPairs.get(prop));
	    	        		channel.setMappedObject(propArrayItem);
	    	        		channel.loadModel(childModel);
						}
					});
	        	}
	        }
	        
            if (model instanceof IXmlModel) {
		        for (final String prop : arrayTxtChildPairs.keySet()) {
		        	if (model2Obj) {
        				String childName = arrayTxtChildPairs.get(prop);
        				model2ObjectArray((ITreeModel) model, obj, prop, childName, new Model2ObjectAction(){
							public Object run(ITreeModel childNode, Object obj) {
								Class<?> rtType = arrayTxtChildClassPairs.get(prop);
								if (rtType == null) rtType = String.class;
								
	        					String text = ((IXmlModel)childNode).getText();
	        					return Utils.getTypedProperty(rtType, text);
							}
    					});
		        	} else { 
	                    objArray2Model((ITreeModel)model, obj, prop, new Object2ModelAction(){
							public void run(ITreeModel propsNode, Object propArrayItem) {
		        				((IXmlModel)propsNode).addChild(arrayTxtChildPairs.get(prop), String.valueOf(Utils.getTypedProperty(String.class, propArrayItem)));
							}
						});
		        	}
		        }
            }
		}
	}
	
	protected void clearObjectPropertyArray(Object obj, String prop) {
		if (obj != null && Assert.isNotNullNotEmpty(prop)) {
			boolean cleaned = false;
			if (arrayPropClearMethodNames.containsKey(prop)) {
				Method method = MethodUtils.getMethod(obj.getClass(), arrayPropClearMethodNames.get(prop));
				if (method != null) {
					try {
						MethodUtils.invokeMethod(obj, method);
						cleaned = true;
					} catch (Exception e) { 	
						Platform.getLogManager().error("Fail to execute method " + arrayPropClearMethodNames.get(prop) + " of object" + obj.getClass(), e, this);
					}
				}
			}
			
			if (!cleaned) {
				try {
					Field field = getFieldSilently(obj.getClass(), prop);
					
					BeanChannelHelper.clearObjectPropertyArray(obj, field);
				} catch (Exception e) { 	
					Platform.getLogManager().error("Fail to clear property " + prop + " of object" + obj.getClass(), e, this);
				}
			}
		}
	}
	
	
	protected void objArray2Model(ITreeModel model, Object obj, String prop, Object2ModelAction runnable) {
		ITreeModel propsNode = (ITreeModel) model;
		if (Assert.isNotNullNotEmpty(getArrayPropertyNodeName(prop))) {
    		((ITreeModel)model).removeTreeChild(getArrayPropertyNodeName(prop));
    	    propsNode = ((IXmlModel)model).getFirstTreeChild(getArrayPropertyNodeName(prop));
    		if (propsNode == null) {
    			propsNode = ((IXmlModel)model).addChild(getArrayPropertyNodeName(prop));
    		}
		}
		
		Object[] values = new Object[0];
		Method method = MethodUtils.getMethod(obj.getClass(), arrayPropGetMethodNames.get(prop));
		if (method != null) {
			values = BeanChannelHelper.getObjectPropertyArray(obj, method);
		} else {
			Field field = getFieldSilently(obj.getClass(), prop);
			if (field != null) {
				try {
					boolean accessible = field.isAccessible();
					if (!accessible) {
						field.setAccessible(true);
					}				
					Object valuesObj = field.get(obj);				
					field.setAccessible(accessible);
	
					if (valuesObj != null) {
						if (Collection.class.isAssignableFrom(valuesObj.getClass())) {
							values = ((Collection<?>)valuesObj).toArray();
						} else if (Object[].class.isAssignableFrom(valuesObj.getClass())) {
							values = (Object[]) valuesObj;
						} else {
							throw new ErrorCodeRuntimeException(ErrorCodes.SMODEL_002);
						}
					}
				} catch (IllegalAccessException e) {
					//ignore, the access exception will not throw
				}
			}
		}
        for (Object propArrayItem : values) {
			if (propArrayItem != null) {
				runnable.run(propsNode, propArrayItem);
			}
		}
	}
	
    protected String getArrayPropertyNodeName(String fieldName) {
    	if (arrayPropsNodeNames.containsKey(fieldName)) {
    		return arrayPropsNodeNames.get(fieldName);
    	}
    	return fieldName;
    }    

	protected static interface Object2ModelAction {
		public void run(ITreeModel propsNode, Object propArrayItem);
	} 

	protected static interface Model2ObjectAction {
		public Object run(ITreeModel childNode, Object obj);
	}
	
	private static Field getFieldSilently(Class<?> clazz, String fieldName) {
		if (Assert.isNotNullNotEmpty(fieldName) && clazz != null) {
			try {
				return clazz.getDeclaredField(fieldName);
			} catch (Exception e) {
				//
			}
		}
		return null;
	}
	
 
	protected static String getDefaultGetArrayMethodName(String fieldName) {
		return "get" + Utils.upStringFirstChar(fieldName);
	}
	
	protected static String getDefaultSetArrayMethodName(String fieldName) {
		return "set" + Utils.upStringFirstChar(fieldName);
	}
	
	protected static String getDefaultArrayChildFullName(String fieldName) {
		if (fieldName.length() <= 1
			|| !fieldName.endsWith("s")) {
			return fieldName;
		}
		return fieldName.substring(0, fieldName.length() - 1);
	}
}
